package pers.cfeng.server.dataManage.logger;

import pers.cfeng.common.DatabaseException;
import pers.cfeng.common.ErrorCode;
import pers.cfeng.server.utils.ByteBufferParser;
import pers.cfeng.server.utils.FaultHandler;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 日志文件的读写
 * 整个日志的checkSum为 各条日志checkSum之和
 */

public class LoggerImpl implements Logger {

    private RandomAccessFile raf;
    private FileChannel fc;
    private Lock lock;

    private long position; //日志指针位置
    private long fileSize; //初始化的时候记录的文件大小，log操作不会更新
    private int TChecksum; //整个log的校验和

    public LoggerImpl(RandomAccessFile raf, FileChannel fc) {
        this.raf = raf;
        this.fc = fc;
        lock = new ReentrantLock();
    }

    public LoggerImpl(RandomAccessFile raf, FileChannel fc, int TChecksum) {
        this.raf = raf;
        this.fc = fc;
        lock = new ReentrantLock();
        this.TChecksum = TChecksum;
    }

    //初始化日志文件： 进行校验
    void init() {
        long size = 0;
        try {
            size = raf.length();
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        //比首部校验和都小，不满足
        if(size < 4) {
            FaultHandler.forcedStop(new DatabaseException(ErrorCode.BAD_LOG_FILE));
        }
        //校验和计算
        ByteBuffer raw = ByteBuffer.allocate(4);
        try {
            fc.position(0);
            fc.read(raw);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        //文件中的校验和
        this.TChecksum = ByteBufferParser.parseInt(raw.array());
        this.fileSize = size;
        //如果结尾存在未写完的日志，去掉检查
        checkAndRemoveTail();
    }

    //不断迭代修改TCheck和position
    private void checkAndRemoveTail() {
        //position重置
        rewind();
        int TCheck = 0;
        while(true) {
            byte[] log = internNext();
            if(log == null) //读取到最后一条文件的时候跳出
                break;
            TCheck = calChecksum(TCheck, log);
        }
        //判断校验和
        if(TCheck != TChecksum) {
            FaultHandler.forcedStop(new DatabaseException(ErrorCode.BAD_LOG_FILE));
        }
        //删除掉最后一个badTail，直接从当前position停留的之后截断
        try {
            truncate(position);
        } catch (Exception e) {
            FaultHandler.forcedStop(e);
        }

        try {
            raf.seek(position);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        //初始化校验操作之后需要重置position指针
        rewind();
    }

    /**
     * 修改首部的校验和
     */
    private void updateTChecksum(byte[] log) {
        this.TChecksum = calChecksum(this.TChecksum, log);
        try {
            fc.position(0);
            fc.write(ByteBuffer.wrap(ByteBufferParser.int2Byte(TChecksum)));
            fc.force(false);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
    }

    //将data字节序列封装为满足要求的日志序列[size][checkSum][data]
    private byte[] warpLog(byte[] data) {
        byte[] checkSum = ByteBufferParser.int2Byte(calChecksum(0, data));
        byte[] size = ByteBufferParser.int2Byte(data.length);
        return ByteBufferParser.mergeBytes(size, checkSum, data);
    }


    /**
     * 获取当前日志， 依靠指针position进行迭代， 读取当前条时，要先判断是否为最后一条日志(不完善)
     *  直接返回null， 之后读取该条日志，进行checksum校验，position变化到下一条日志的起始
     */
    private byte[] internNext() {
        if(position + LoggerConstant.OFFSET_DATA >= fileSize) {
            //该条日志的SIZE + CHECKSUM已经超过size，说明读取完了
            return null;
        }
        ByteBuffer temp = ByteBuffer.allocate(4);
        try {
            fc.position(position);
            fc.read(temp);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        int size = ByteBufferParser.parseInt(temp.array()); //日志data长度
        if(position + size + LoggerConstant.OFFSET_DATA > fileSize) {
            return  null;//当前日志没写完，data不满足，所以不读取了
        }
        //一条日志的长度
        ByteBuffer buffer = ByteBuffer.allocate(LoggerConstant.OFFSET_DATA + size);
        try {
            fc.position(position);
            fc.read(buffer);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        byte[] log = buffer.array();
        //读取日志后先计算日志里的校验和是否正确
        int calculateCheckSum = calChecksum(0, Arrays.copyOfRange(log, LoggerConstant.OFFSET_DATA, log.length));
        int checkSumInLog = ByteBufferParser.parseInt(Arrays.copyOfRange(log, LoggerConstant.OFFSET_CHECKSUM, LoggerConstant.OFFSET_DATA));
        if(calculateCheckSum != checkSumInLog)
            return null;
        position += log.length;
        return log;
    }

    //日志内部校验和就是对于该条日志按照设定的算法求和
    private int calChecksum(int TCheck, byte[] log) {
        for(byte b: log)
            TCheck = TCheck * LoggerConstant.seed + b;
        return TCheck;
    }

    /**
     * @param data  日志内容
     * 新写入日志
     */
    @Override
    public void log(byte[] data) {
        byte[] log = warpLog(data);
        ByteBuffer buffer = ByteBuffer.wrap(log);
        lock.lock();
        try {
            fc.position(fc.size()); //position为结尾开始写
            fc.write(buffer);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        } finally {
            lock.unlock();
        }
        updateTChecksum(log);
    }

    /**
     *
     * @param x  需要删除后面内容的位置
     * 直接调用的fileChannel的truncate方法
     */
    @Override
    public void truncate(long x) throws Exception {
        lock.lock();
        try {
            fc.truncate(x);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 获取下一条日志的内容data
     */
    @Override
    public byte[] next() {
        lock.lock();
        try {
            byte[] log = internNext();
            if(log == null)
                return null;
            return Arrays.copyOfRange(log, LoggerConstant.OFFSET_DATA, log.length);
        } finally {
            lock.unlock();
        }
    }

    //重置position指针
    @Override
    public void rewind() {
        this.position = LoggerConstant.OFFSET_CHECKSUM;
    }

    @Override
    public void close() {
        try {
            fc.close();
            raf.close();
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
    }
}
